#region Imported Namespaces
using System;
using System.Collections;
using System.Configuration;using System.DirectoryServices;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Microsoft.Win32;

using Crypt = PHSRAG.Utility.Crypt;
using Tracer = PHSRAG.Utility.Tracer;
#endregion

//[assembly:SecurityPermissionAttribute(SecurityAction.RequestMinimum, UnmanagedCode = true)]
//[assembly:PermissionSetAttribute(SecurityAction.RequestMinimum, Name = "FullTrust")]
namespace PHSRAG.Security
{
	/// <summary>
	/// The Authenticator class is used to authenticate the credentials supplied.
	/// </summary>
	public class Authenticator
	{
		#region Public Types
		/// <summary>
		/// The LogonProvider enumeration aliases defintions related to logon providers from the Win32 header files.
		/// </summary>
		public enum LogonProvider
		{
			/// <summary>
			/// Use the standard logon provider for the system. The default security provider is NTLM.
			/// Windows XP: The default provider is negotiate, unless you pass NULL for the domain name and the user name'
			/// is not in UPN format. In this case the default provider is NTLM.
			/// </summary>
			Default = 0,	// LOGON32_PROVIDER_DEFAULT

			/// <summary>
			/// Use the Windows NT 3.5 logon provider.
			/// </summary>
			WinNT35 = 1,	// LOGON32_PROVIDER_WINNT35

			/// <summary>
			/// Use the NTLM logon provider.
			/// </summary>
			WinNT40 = 2,	// LOGON32_PROVIDER_WINNT40

			/// <summary>
			/// For Windows XP/2000:  Use the negotiate logon provider.
			/// </summary>
			WinNT50 = 3		// LOGON32_PROVIDER_WINNT50
		}

		/// <summary>
		/// The LognType enumeration aliases definitions related to logon from the Win32 header files.
		/// </summary>
		public enum LogonType
		{
			/// <summary>
			/// This logon type is intended for users who will be interactively using the computer, such as a user being
			/// logged on by a terminal server, remote shell, or similar process. This logon type has the additional
			/// expense of caching logon information for disconnected operation, and is therefore inappropriate for some
			/// client/server applications, such as a mail server.
			/// </summary>
			Interactive = 2,	// LOGON32_LOGON_INTERACTIVE

			/// <summary>
			/// This logon type is intended for high performance servers to authenticate clear text passwords.
			/// </summary>
			Network = 3,		// LOGON32_LOGON_NETWORK

			/// <summary>
			/// This logon type is intended for batch servers, where processes may be executing on behalf of a user
			/// without their direct intervention; or for higher performance servers that process many clear-text
			/// authentication attempts at a time, such as mail or web servers.
			/// </summary>
			Batch = 4,			// LOGON32_LOGON_BATCH

			/// <summary>
			/// Indicates a service-type logon. The account provided must have the service privilege enabled.
			/// </summary>
			Service = 5,		// LOGON32_LOGON_SERVICE

			/// <summary>
			/// This logon type is intended for GINA DLLs logging on users who will be interactively using the computer.
			/// This logon type allows a unique audit record to be generated that shows when the workstation was unlocked.
			/// </summary>
			Unlock = 7,			// LOGON32_LOGON_UNLOCK

			/// <summary>
			/// This logon type preserves the name and password in the authentication packages, allowing the server to
			/// make connections to other network servers while impersonating the client. This allows a server to accept
			/// clear text credentials from a client, call LogonUser, verify that the user can access the system across
			/// the network, and still communicate with other servers.
			/// </summary>
			ClearText = 8,		// LOGON32_LOGON_NETWORK_CLEARTEXT

			/// <summary>
			/// This logon type allows the caller to clone its current token and specify new credentials for outbound
			/// connections. The new logon session has the same local identity, but uses different credentials for other
			/// network connections. 
			/// </summary>
			NewCredentials = 9	// LOGON32_LOGON_NEW_CREDENTIALS
		}
		#endregion

		#region Interop
		[DllImport("Advapi32.dll", SetLastError = true)]
		private static extern bool LogonUser(string userName, string domain, string password,
			int logonType, int logonProvider, ref IntPtr userToken);

		[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
		private extern static bool CloseHandle(IntPtr handle);
		#endregion

		#region Constants
		private const string Domain = "partners";
		private const string DomainPath = "LDAP://partners.org/DC=partners,DC=org";
		private const string UserPath = "LDAP://partners.org/CN={0},CN=Users,DC=partners,DC=org";
		#endregion

		#region Public Class Methods
		/// <summary>
		/// Get LDAP attributes for the specified credentials and set of properties.
		/// </summary>
		/// <param name="userName">Login name of a user in the ActiveDirectory</param>
		/// <param name="propertyNames">Set of properties whose attributes are required</param>
		/// <returns>LDAP object containing the attributes</returns>
		public static SearchResult GetAttributes(string userName, params string[] propertyNames)
		{
			using (DirectoryEntry entry = new DirectoryEntry(DomainPath))
			{
				using (DirectorySearcher search = new DirectorySearcher(entry))
				{
					search.Filter = string.Format("(&(objectClass=user)(SAMAccountName={0}))", userName);
					if ((propertyNames != null) && (propertyNames.Length > 0))
						foreach (string propertyName in propertyNames)
							search.PropertiesToLoad.Add(propertyName);
					return search.FindOne();
				}
			}
		}

		/// <summary>
		/// Get the set of groups to which the specified user belongs to.
		/// </summary>
		/// <param name="userName">Login name of a user in the ActiveDirectory</param>
		/// <returns>Array of strings containg all the group membership (could be null)</returns>
		public static string[] GetGroups(string userName)
		{
			string[] groups = null;
			using (DirectoryEntry entry = new DirectoryEntry(string.Format(UserPath, userName)))
			{
				PropertyValueCollection propertyValues = entry.Properties["memberOf"];
				if (propertyValues.Count > 0)
				{
					Regex regex = new Regex(@"[^=]+=(?<groupName>[^,]+)", RegexOptions.Compiled);

					groups = new string[propertyValues.Count];
					for (int i = 0, count = propertyValues.Count; i < count; ++i)
					{
						Match match = regex.Match(propertyValues[i].ToString());
						if (match.Success)
							groups[i] = match.Groups["groupName"].Value;
					}
				}
			}

			return groups;
		}

		/// <summary>
		/// Get the set of groups to which the specified user belongs to. Each group is delimited by the specified delimiter.
		/// </summary>
		/// <param name="userName">Login name of a user in the ActiveDirectory</param>
		/// <param name="delimiter">Delimiter character</param>
		/// <returns>String containg all the group membership delimited by the specified delimiter (could be empty)</returns>
		public static string GetGroups(string userName, char delimiter)
		{
			StringBuilder sb = new StringBuilder();
			string delimiterString = Convert.ToString(delimiter);

			foreach (string group in GetGroups(userName))
				sb.AppendFormat("{0}{1}", sb.Length > 0 ? delimiterString : string.Empty, group);
			return sb.ToString();
		}

		/// <summary>
		/// Impersonate the special account "Peter North".
		/// </summary>
		/// <returns>The WindowsImpersonationContext object that identifies the impersonation</returns>
		public static WindowsImpersonationContext Impersonate()
		{
			string configFile = null;
			Assembly executingAssembly = Assembly.GetExecutingAssembly();
			if (executingAssembly.GlobalAssemblyCache)
			{
				using(RegistryKey key = Registry.LocalMachine.OpenSubKey(@"Software\PHSRAG\PHSRAG.Security", false))
				{
					configFile = key.GetValue("ConfigFilePath", null) as string;
				}
			}
			else
				configFile = executingAssembly.Location + @".config";

			if (!File.Exists(configFile))
				throw new Exception(string.Format("Security.Authenticator.Impersonate: configFile '{0}' does not exist", configFile));

			XmlDocument doc = new XmlDocument();
			doc.Load(configFile);

			IDictionary appSettings = (IDictionary)(new DictionarySectionHandler()).Create(null, null, doc.GetElementsByTagName("appSettings")[0]);
			string userName = (string)appSettings["Identity.Name"];
			string password = Crypt.DecryptFromString((string)appSettings["Identity.Password"]);

			return ImpersonateUser(userName, password, LogonType.NewCredentials, LogonProvider.WinNT50);
		}

		/// <summary>
		/// Impersonates the specfied user using the specified logon type and provider. The logon will be attempted into the
		/// specified domain first, and if that fails, into the local machine. If both these attempts fail, an exception is
		/// thrown.
		/// </summary>
		/// <param name="userName">Username part of the credential</param>
		/// <param name="password">Password part of the credential</param>
		/// <param name="logonType">One of the LogonType enumeration</param>
		/// <param name="logonProvider">One of the LogonProvider enumeration</param>
		/// <returns>The WindowsImpersonationContext object that identifies the impersonation</returns>
		public static WindowsImpersonationContext ImpersonateUser(string userName, string password, LogonType logonType,
			LogonProvider logonProvider)
		{
			IntPtr userToken = IntPtr.Zero;
			StringBuilder sb = new StringBuilder();

			string[] domainList = { Domain, Environment.MachineName };
			foreach (string domain in domainList)
			{
//				Tracer.Trace(string.Format("Attempting impersonation for: {0}\\{1}, {2}/{3}",
//					domain, userName, Enum.GetName(typeof(Authenticator.LogonProvider), logonProvider),
//					Enum.GetName(typeof(Authenticator.LogonType), logonType)));

				if (LogonUser(userName, Domain, password, (int)logonType, (int)logonProvider, ref userToken))
					break;
				sb.AppendFormat("{0} ({1}\\{2}, {3}, {4})\r\n",
					PHSRAG.NativeInterface.MessageCatalog.GetErrorMessage(Marshal.GetLastWin32Error()), domain, userName,
					Enum.GetName(typeof(LogonType), logonType), Enum.GetName(typeof(LogonProvider), logonProvider));
			}

			if (userToken == IntPtr.Zero)
				throw new Exception(sb.ToString());
			
			WindowsImpersonationContext impersonationContext = (new WindowsIdentity(userToken)).Impersonate();
			CloseHandle(userToken);
			return impersonationContext;
		}

		/// <summary>
		/// This method stops the impersonation and reverts to the original login context.
		/// </summary>
		/// <param name="impersonationContext">The impersonation context returned by a call to ImpersonateUser</param>
		public static void RevertUser(WindowsImpersonationContext impersonationContext)
		{
			if (impersonationContext != null)
				impersonationContext.Undo();
		}

		/// <summary>
		/// Validate the credentials supplied against the ActiveDirectory entries. Throws an exception
		/// if authentication fails.
		/// </summary>
		/// <param name="userName">Username part of the credential</param>
		/// <param name="password">Password part of the credential</param>
		public static void ValidateCredentials(string userName, string password)
		{
			using (DirectoryEntry entry = new DirectoryEntry(DomainPath, string.Format(@"{0}\{1}", Domain, userName), password,
					   AuthenticationTypes.Secure))
			{
				Object obj = entry.NativeObject;	// Bind to the native AdsObject to force authentication.
				using (DirectorySearcher search = new DirectorySearcher(entry))
				{
					search.Filter = string.Format(@"(SAMAccountName={0})", userName);
					search.PropertiesToLoad.Add("cn");
					SearchResult result = search.FindOne();
					if (result == null)
						throw new Exception("Error searching for the specified credentials");
				}
			}
		}
		#endregion

		#region Public Instance Methods
		// This assembly is registered for COM-interop. In that context, none of the static methods are exposed to
		// automation clients. To allow such clients, the following instance methods are provided.

		/// <summary>
		/// Impersonates the specfied user if the specified credentials are valid.
		/// </summary>
		/// <param name="userName">Username part of the credential</param>
		/// <param name="password">Unecrypted password part of the credential</param>
		/// <param name="logonType">One of the LogonType enumeration</param>
		/// <param name="logonProvider">One of the LogonProvider enumeration</param>
		/// <returns>The WindowsImpersonationContext object that identifies the impersonation</returns>
		public static WindowsImpersonationContext BeginImpersonation(string userName, string password, LogonType logonType,
			LogonProvider logonProvider)
		{
			return ImpersonateUser(userName, password, logonType, logonProvider);
		}

		/// <summary>
		/// Impersonates the specfied user if the specified credentials are valid.
		/// </summary>
		/// <param name="userName">Username part of the credential</param>
		/// <param name="password">Encrypted password part of the credential</param>
		/// <param name="logonType">One of the LogonType enumeration</param>
		/// <param name="logonProvider">One of the LogonProvider enumeration</param>
		/// <returns>The WindowsImpersonationContext object that identifies the impersonation</returns>
		public static WindowsImpersonationContext BeginSecureImpersonation(string userName, string password, LogonType logonType,
			LogonProvider logonProvider)
		{
			ResourceManager rm = new ResourceManager("PHSRAG.Security.Crypt", Assembly.GetExecutingAssembly());
			CultureInfo ci = System.Threading.Thread.CurrentThread.CurrentCulture;
			byte[] key = (byte[])rm.GetObject("Key", ci);
			byte[] iv = (byte[])rm.GetObject("IV", ci);
			if ((key == null) || (iv == null))
				throw new Exception("Invalid Key/IV pair");
				
			return ImpersonateUser(userName, Utility.Crypt.DecryptFromString(password), logonType, logonProvider);
		}

		/// <summary>
		/// This method stops the impersonation and reverts to the original login context.
		/// </summary>
		/// <param name="context">The impersonation context returned by a call to ImpersonateUser</param>
		public static void EndImpersonation(WindowsImpersonationContext context)
		{
			RevertUser(context);
		}

		/// <summary>
		/// Validate the credentials supplied against the ActiveDirectory entries. Throws an exception
		/// if authentication fails.
		/// </summary>
		/// <param name="userName">Username part of the credential</param>
		/// <param name="password">Password part of the credential</param>
		public static void ValidateUser(string userName, string password)
		{
			ValidateCredentials(userName, password);
		}
		#endregion
	}
}

// --- EOF ---
